#
#      Copyright 2011 Tom Miranda
#       
#       Licensed under the Apache License, Version 2.0 (the "License");
#       you may not use this file except in compliance with the License.
#       You may obtain a copy of the License at
#
#          http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing, software
#       distributed under the License is distributed on an "AS IS" BASIS,
#       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#       See the License for the specific language governing permissions and
#       limitations under the License.
#
import threading, time, random, sets, mc, xbmc, recordings, airings, epg, config, sageplayer
from time import sleep
from threading import RLock
from sets import Set

def startWatching(chan, UUID, api, window=14040, control=300):
	if not chan.has_key('StationID'):
		mc.ShowDialogOk("Error - Malformed channel","No StationID is available.")
		return None

	updateStatus(window, control, "Looking for show on channel " + chan['ChannelNumber'] + ".")
	stationID = chan['StationID']
	mc.LogDebug("SageBoxee: StationID = " + str(stationID))

	airingOnNow = getAiringOnChannelNow(stationID, api)
	mc.LogDebug("SageBoxee: Airing on now is " + str(airingOnNow))

	updateStatus(window, control, "Checking if show is already being recorded.")
	isAiringRecording = epg.isAiringRecording(airingOnNow['AiringID'], api)
	mc.LogDebug("SageBoxee: Airing isRecording " + str(isAiringRecording))
	return playAiringItem(airings.airingToListItem(airingOnNow, api), isAiringRecording, UUID, api, window, control, inBackground=True, waitForLoad=True)

# Get the channels from the Sage server if necessary.
def getChannels(channels, api, window=14040, control=300):
	if channels==None or len(channels) == 0:
		updateStatus(window, control, "Getting channel information.")
		mc.LogDebug("SageBoxee: Getting all channels from server.")
		return getAllViewableChannels(api)
	else:
		updateStatus(window, control, "Already have channel information.")
		mc.LogDebug("SageBoxee: Already have channels.")
		return channels
	
# Get all viewable channels, sorted by number.
def getAllViewableChannels(api):
	expr = 'Sort(FilterByBoolMethod(GetAllChannels(), "IsChannelViewable", true), false, "ChannelNumber")'
	return api.EvaluateExpression(expr)

# Get the Airing that's on the specified stationID right now.	
def getAiringOnChannelNow(stationID, api):
	expr = 'GetElement(GetAiringsOnChannelAtTime(GetChannelForStationID("%s"), Time(), Time(), false),0)' % stationID
	return api.EvaluateExpression(expr)	
	
def updateStatus(window, control, message=''):
	if window==0 or control==0:
		return
	try:
		if message==None or message=='':
			mc.GetWindow(window).GetLabel(control).SetLabel('')
			mc.GetWindow(window).GetControl(control).SetVisible(False)
		else:
			mc.GetWindow(window).GetLabel(control).SetLabel(message)
			mc.GetWindow(window).GetControl(control).SetVisible(True)
	except:
		mc.LogDebug("SageBoxee: Window could not be updated " + str(window))
		
# Update the info on the OSD related to the channel that's selected.
def updateOSDChannel(chan):
	if chan.has_key('ChannelNumber'):
		channelNumber = str(chan['ChannelNumber'])
	else:
		channelNumber = '0'
	
	if chan.has_key('ChannelName'):
		channelName = str(chan['ChannelName'])
	else:
		channelName = 'Unknown'

	try:
		mc.GetWindow(14040).GetButton(202).SetLabel(channelNumber+" "+channelName)
	except:
		mc.LogDebug("SageBoxee: Failed to update window 14040.")
	return		

# Checks to see if the player is active for waitTime seconds.  Returns True if it's active at any time.  This is needed to get
# around a bug in the play.IsCaching() method.
def isPlayerActiveWait(UUID='0', waitTime=0):
	waits = waitTime
	while waits >= 0:
		if isPlayerActive(UUID):
			return True
		else:
			sleep(1)
			waits -= 1
	return False

# Will return False if the player is caching.  No way to avoid this since IsCaching() causes the app to crash with a seg fault.
def isPlayerActive(itemUuid='0'):
	player = mc.GetPlayer()
	if player == None:
		return False
	#isActive = player.IsPaused() or player.IsForwarding() or player.IsPlaying() or player.IsRewinding()
	isActive = player.IsPlaying()
	if itemUuid == '0':
		sameUuid = True
	else:
		playingItem = player.GetPlayingItem()
		if playingItem == None:
			sameUuid = False
		else:
			sameUuid = playingItem.GetProperty('UUID')==itemUuid
	return isActive and sameUuid

# Wait for the speficied MediaFile to grow to at least the specified size.	
def waitForSize(mf, size, api, window=0, control=0):
	mc.LogDebug("SageBoxee: Waiting for MediaFile to get to " + str(size))
	updateStatus(window, control, "Buffering.")
	mediaFileID = mf['MediaFileID']
	currentSize = 0
	growing = True
	#tries = 25
	percentDone = 0.0
	expr = 'GetSize(GetMediaFileForID(%s))' % mediaFileID
	while currentSize < size and growing:
		#tries -= 1
		sleep(1)
		newSize = api.EvaluateExpression(expr)
		growing = newSize > currentSize or currentSize == 0	# 0 means it hasn't started, so give it a chance.
		currentSize = newSize
		percentDone = float(currentSize) / float(size)
		updateStatus(window, control, "Buffering (%d%%)" % int(percentDone*100))
	mc.LogDebug("SageBoxee: Size = " + str(currentSize))
	return currentSize >= size

# Plays the specified item which has been constructed from a MediaFile.  Handles multi-segment MediaFiles by converting them into
# a playlist and then playing the playlist. Returns True if the MediaFile is being played in the background, False otherwise.
# This is important because if it's playing in the foreground Boxee will supply the OSD, otherwise we will supply the OSD.
# Can optionally wait for the item to get loaded into the player by specifying waitForload=True.  Defaults to False.
def playMediaFileItem(mediaFileItem, api, window=0, control=0, inBackground=False, waitForLoad=False):
	mc.LogDebug('SageBoxee: Watching a MediaFile, waitforLoad:inBackground ' + str(waitForLoad) + ":" + str(inBackground))
	updateStatus(window, control, "Launching player.")
	
	playingInBackground = inBackground
	
	# Multi-segment files can't be played in the background.
	if int(mediaFileItem.GetProperty('NumberOfSegments')) > 1:
		playingInBackground = False
		
	sageplayer.play(mediaFileItem, playingInBackground, api)
	
	#if int(mediaFileItem.GetProperty('NumberOfSegments')) > 1:
	#	mc.LogDebug("SageBoxee: Playing a multi-segment MediaFile in the foreground.")
	#
	#	# Clear any other playlists
	#	mc.PlayList(mc.PlayList.PLAYLIST_VIDEO).Clear()
	#	mc.PlayList(mc.PlayList.PLAYLIST_MUSIC).Clear()
	#
	#	# Create a video playlist.
	#	playlist = mc.PlayList(mc.PlayList.PLAYLIST_VIDEO)
	#
	#	# Add this item to the playlist and play it.
	#	playlist = recordings.addMultiSegmentItemToPlaylist(mediaFileItem, playlist)
	#	playlist.Play(0)
	#	playingInBackground = False
	#
	#else:
	#	player = mc.GetPlayer()
	#	if inBackground:
	#		mc.LogDebug("SageBoxee: Playing a MediaFile in the background.")
	#		player.PlayInBackground(mediaFileItem)
	#		playingInBackground = True
	#	else:
	#		mc.LogDebug("SageBoxee: Playing a MediaFile in the foreground.")		
	#		player.Play(mediaFileItem)
	#		playingInBackground = False
			
	# After issuing the PlayInBackground() or Play() commands, Boxee takes about 30 seconds to actually load the item and begin
	# playback.  Not much we can do except display the wait dialog and wait.
	if waitForLoad:
		mc.LogDebug("SageBoxee: Waiting for MediaFile to load.")
		updateStatus(window, control, "Waiting for Boxee to load the show.")
		player = mc.GetPlayer()
		loaded = False
		for i in range(config.getLiveTvWaitForLoadSecs(30)*5):
			sleep(0.200)
			playingItem = player.GetPlayingItem()
			if playingItem.GetPath() != '':
				mc.LogDebug("SageBoxee: Item has loaded.")
				loaded = True
				break
		if not loaded:
			updateStatus(window, control)
			mc.LogDebug("SageBoxee: Item failed to load.")
			mc.ShowDialogOk("Error.","The MediaFile did not load.")

	return playingInBackground

# Plays the specified item which has been constructed from an Airing that may or may not have a MediaFile and may or may not
# be recording.  If isAiringRecording is set to True it will assume the Airing is recording and just play the corresponding
# MediaFile.  If isAiringRecording is set to False it will start the recording, play it, and when playback terminates delete
# the MediaFile that was produced.	
def playAiringItem(item, isAiringRecording, itemUuid, api, window=0, control=0, inBackground=True, waitForLoad=True):

	# FIXME TEST!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
	#expr = 'GetMediaFileForAiring(GetAiringForID(12779610))'
	#MF = api.EvaluateExpression(expr)
	#item = recordings.mediaFileToListItem(MF)
	#isAiringRecording = True
	
	okToPlay = True
	
	# FIXME - Won't work in the case where the show on the target channel was partially recorded and stopped.  This will just 
	# play the recorded pieces and not restart the recording....
	if item.GetProperty('MediaFileID') != '':
		# If the item has a MediaFileID it means we can ignore isAiringRecording and just play the file.
		mc.LogDebug("SageBoxee: Airing has a MediaFile.")
		updateStatus(window, control, "Getting ready to play MediaFile.")
		mediaFileListItem = item
		mediaFileListItem.SetProperty('UUID', itemUuid)
	else:
		# If the Airing is not recording try to start it.
		mc.LogDebug('SageBoxee: Watching an Airing')
	
		if not isAiringRecording:
			# Start the recording and wait for a MediaFile to be created.
			mc.LogDebug("SageBoxee: Airing was not recording. About to recordAndWait()")
			updateStatus(window, control, "Tuning " + item.GetProperty('ChannelName') + ".")
			mc.ShowDialogWait()
			mf = api.recordAndWait(item)
			mc.HideDialogWait()
			
			if  mf==None:
				# If the recording failed to start set isAiringRecording to True so that the deleter thread is never started.
				mc.LogDebug("SageBoxee: No MediaFile for Airing.")
				updateStatus(window, control)
				mc.ShowDialogOk("Error.", "The selected show could not be watched because the recording did not start. This usually means no tuners are available.")
				okToPlay = False
				playingInBackground = False
				isAiringRecording = True
			else:
				# If the recording started wait for the file to grow a little bit before launching the player.
				if not waitForSize(mf, config.getLiveTvBufferSize(), api, window, control):
					mc.LogDebug("SageBoxee: MediaFile failed to grow.")
					updateStatus(window, control)
					mc.ShowDialogOk("Error.", "The selected show could not be watched. The recording started but has stalled.")
					okToPlay = False
					playingInBackground = False
					mediaFileListItem = recordings.mediaFileToListItem(mf)
		else:
			# If the airing was already recording get the MediaFile object so we can pass it to the player.
			mc.LogDebug("SageBoxee: Airing was already recording.")
			updateStatus(window, control, "Tuning the show in progress.")
			mf = epg.getMediaFileForAiring(item, api)
		
		if okToPlay:
			# Convert the MediaFile object to something the player can handle.
			updateStatus(window, control, "Getting ready to play.")
			mediaFileListItem = recordings.mediaFileToListItem(mf)
			mediaFileListItem.SetProperty('UUID', itemUuid)
			mc.LogDebug("SageBoxee: Getting ready to play MediaFileID=" + mediaFileListItem.GetProperty("MediaFileID"))
	
	if okToPlay:
		playingInBackground = playMediaFileItem(mediaFileListItem, api, window, control, inBackground, waitForLoad)
	
	# If this Airing was already recording there is no need to wait around and cleanup anything.
	if isAiringRecording:
		mc.LogDebug("SageBoxee: Airing was already recording, returning to caller.")
	else:
		# If we're playing a playlist in the foreground we must return False but we still need to continue to monitor the 
		# airing to delete the partial file.
		mc.LogDebug("SageBoxee: Will monitor and delete.")
		deleter = MediaFileDeleter(mediaFileListItem, api)
		deleter.start()
		deleter.setName("SageBoxeeDeleter")
	
	return playingInBackground

# Return the active capture devices but name will include quality it's recording.	
def getActiveCaptureDevices(api):
	activeCaptureDevices = []
	recordingMediaFiles = api.GetCurrentlyRecordingMediaFiles()
	if recordingMediaFiles == None or len(recordingMediaFiles)==0:
		return activeCaptureDevices
	for mf in recordingMediaFiles:
		item = recordings.mediaFileToListItem(mf)
		encoder = item.GetProperty('MediaFileEncoding')
		if encoder != None and encoder != '':
			activeCaptureDevices.append(encoder)
	mc.LogDebug("SageBoxee: Active capture devices " + str(activeCaptureDevices))
	return activeCaptureDevices

# Get an array of capture devices that are not currently recording something.	
def getAvailableCaptureDevices(api):
	availableDevices = []
	
	# Get all of the devices that are configured.
	allDevices = api.GetActiveCaptureDevices()
	mc.LogDebug("SageBoxee: All devices = " + str(allDevices))
	
	# Check for special case of no devices configured.
	if allDevices == None or len(allDevices) == 0:
		return availableDevices
		
	for device in allDevices:
		expr = 'GetCaptureDeviceCurrentRecordFile("%s")' % device
		mf = api.EvaluateExpression(expr)
		if mf == None or mf=='':
			availableDevices.append(device)

	return availableDevices
	
def getConfiguredInputsForDevice(device, api):
	expr = 'DataIntersection(DataUnion(GetConfiguredCaptureDeviceInputs()), DataUnion(GetCaptureDeviceInputs("%s")))' % device
	return api.EvaluateExpression(expr)
	
def getDeviceInputsForAvailableCaptureDevices(api):
	allInputs = []
	availableDevices = getAvailableCaptureDevices(api)
	if availableDevices == None or len(availableDevices) == 0:
		return allInputs
	for device in availableDevices:
		inputs = getConfiguredInputsForDevice(device, api)
		mc.LogDebug("SageBoxee: device and inputs = " + device + "->" + str(inputs))
		if inputs != None and len(inputs) > 0:
			for inp in inputs:
				allInputs.append(inp)
	mc.LogDebug("SageBoxee: All inputs = " + str(allInputs))
	return allInputs
	
def getAvailableLineups(inputs, api):
	lineups = []
	if inputs==None or len(inputs)==0:
		return lineups
	for inp in inputs:
		expr = 'GetLineupForCaptureDeviceInput("%s")' % inp
		lineup = api.EvaluateExpression(expr)
		if lineup != None and lineup != '':
			lineups.append(lineup)
	mc.LogDebug("SageBoxee: Available lineups = " + str(lineups))
	return lineups
	
def getChannelsThatCanBeWatchedLive(api):
	watchableChannels = Set()
	
	allChannels = api.GetAllChannels()
	if allChannels==None or len(allChannels)==0:
		return watchableChannels
		
	updateStatus(14040, 300, "Looking for available tuners - Getting available capture devices.")
	availableInputs = getDeviceInputsForAvailableCaptureDevices(api)
	if availableInputs==None or len(availableInputs)==0:
		return watchableChannels
		
	updateStatus(14040, 300, "Looking for available tuners - Getting available lineups.")
	allLineups = getAvailableLineups(availableInputs, api)
	if allLineups==None or len(allLineups)==0:
		return watchableChannels
		
	updateStatus(14040, 300, "Looking for available tuners - Getting available channels on lineup.")
	for chan in allChannels:
		updateOSDChannel(chan)
		stationID = chan['StationID']
		for lineup in allLineups:
			expr = 'IsChannelViewableOnLineup(GetChannelForStationID(%s), "%s")' % (stationID, lineup)
			if api.EvaluateExpression(expr):
				watchableChannels.add(stationID)
				break
	mc.LogDebug("SageBoxee: Found watchable channels " + str(watchableChannels))
	return watchableChannels
	
class MediaFileDeleter(threading.Thread):
	_api = None
	_mediaFileItem = None
	
	def __init__(self, item, api):
		threading.Thread.__init__(self)
		self._mediaFileItem = item
		self._api = api
		
	def run(self):
		mc.LogDebug("SageBoxee: Starting MediaFileDeleter thread.")
		
		# Wait until the mediaFileItem is no longer playing.
		itemUuid = self._mediaFileItem.GetProperty('UUID')
		mc.LogDebug("SageBoxee: Will monitor UUID " + itemUuid)
		
		while isPlayerActive(itemUuid):
			sleep(1)
			
		# Cancel the manual record and remove the MediaFile if the MediaFile was not already a recording in progress. Need a small
		# delay between canceling the recording and deleting the MediaFile so Sage has time to actually stop the recording.
		mc.LogDebug("SageBoxee: Player is no longer active. Canceling recording and removing artifacts.")
		self._api.CancelRecord(self._mediaFileItem)
		sleep(5)
		if not self._api.DeleteFileWithoutPrejudice(self._mediaFileItem):
			mc.LogInfo("SageBoxee: Warning - Failed to remove MediaFile.")
		
		mc.LogDebug("SageBoxee: Ending MediaFileDeleter thread.")
		
class OSDHider(threading.Thread):
	_window = None
	_closetime = None
	_controls = None
	_lock = None
	_terminate = False
	_secsBeforeClosing = None
	
	def __init__(self, window, closetime, controls):
		threading.Thread.__init__(self)
		self._window = window
		self._controls = controls
		self._closetime = closetime
		self._lock = RLock()
		self._terminate = False
		
	def run(self):
		mc.LogDebug("SageBoxee: Starting OSDHider thread.")
		
		while not self._terminate:
			sleep(1)
			try:
				self._lock.acquire()
				
				# No need to do anything if the OSD has been closed and the countdown timer has not been reset or the closetime
				# is set to 0 (meaning disabled).
				if self._secsBeforeClosing > 0 and self._closetime > 0:
				
					# Reduce the countdown timer and if it has reached zero make the OSD controls invisible.
					self._secsBeforeClosing -= 1
					if self._secsBeforeClosing <= 0:
						mc.LogDebug("SageBoxee: Autohiding OSD.")
						for control in self._controls:
							try:
								mc.GetWindow(self._window).GetControl(control).SetVisible(False)
							except:
								# Setting the visibility may fail for many reasons that are not bad so we will just ignore any errors.
								pass
			finally:
				self._lock.release()
		
		mc.LogDebug("SageBoxee: Ending OSDHider thread.")
		
	# Used to terminate the thread.
	def setTerminate(self, val):
		self._lock.acquire()
		self._terminate = val
		self._lock.release()
		
	# Changes the auto close time.
	def setCloseTime(self, val):
		self._lock.acquire()
		self._closetime = val
		self._lock.release()
		
	# Restart the countdown to close.  Invoke this each time a keystroke is processed.
	def resetCountdown(self):
		#mc.LogDebug("SageBoxee: OSD activity.")
		self._lock.acquire()
		self._secsBeforeClosing = self._closetime
		self._lock.release()
		
	# Set countdown timer to an arbitrary value.
	def setCountdown(self, val):
		self._lock.acquire()
		self._secsBeforeClosing = val
		self._lock.release()
		
	# Inspects the OSD and returns True if any of the controls are visible.
	def isVisible(self):
		self._lock.acquire()
		for control in self._controls:
			if mc.GetWindow(self._window).GetControl(control).IsVisible():
				self.makeVisible()
				self._lock.release()
				return True
		self._lock.release()
		return False
		
	# Make the controls visible again.
	def makeVisible(self, channel=None):
		self._lock.acquire()
		for control in self._controls:
			try:
				mc.GetWindow(self._window).GetControl(control).SetVisible(True)
			except:
				# Setting the visibility may fail for many reasons that are not bad so we will just ignore any errors.
				mc.LogDebug("SageBoxee: Failed to make control visible.")
				pass
		if channel != None:
			updateOSDChannel(channel)	
		self._lock.release()
